Rails初始化过程 启动分析

问题:
1、为什么项目路径任意目录下都可以执行rails s,启动项目
2、rails s和rails server有什么区别,以及是如何实现的
3、终端是执行ctrl + c后,rails做了什么
4、是何时加载gem的
5、rails s 之后都做了哪些工作

1. gem ‘railties’ 是rails的核心gem, 处理rails程序的引导入口,管理rails的命令行接口

1.1 执行rails s 首先会执行 railties下的 bin/rails 可执行文件,内容如下

1
2
3
4
5
6
7
8
9
#!/usr/bin/env ruby
git_path = File.expand_path('../../../.git', __FILE__)
if File.exist?(git_path)
railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path)
end
require "rails/cli"

加载了 rails/cli,此文件加载了 rails/app_rails_loader,并执行exec_app_rails方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def exec_app_rails
original_cwd = Dir.pwd
loop do
if exe = find_executable
contents = File.read(exe)
if contents =~ /(APP|ENGINE)_PATH/
exec RUBY, exe, *ARGV
break # non reachable, hack to be able to stub exec in the test suite
elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
$stderr.puts(BUNDLER_WARNING)
Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
require File.expand_path('../boot', APP_PATH)
require 'rails/commands'
break
end
end
# If we exhaust the search there is no executable, this could be a
# call to generate a new application, so restore the original cwd.
Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
# Otherwise keep moving upwards in search of an executable.
Dir.chdir('..')
end
end
def find_executable
EXECUTABLES.find { |exe| File.file?(exe) }
end
end

此方法会找到项目路径下的,bin/rails文件,并执行

1
exec RUBY,bin/rails,s

bin/rails文件如下

1
2
3
4
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

config/boot 会加载 bundler/setup,Bundler 通过它设置 Gemfile 中依赖关系的加载路径。
执行完config/boot文件后,继续加载 rails/commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
}
command = ARGV.shift
command = aliases[command] || command
require 'rails/commands/commands_tasks'
Rails::CommandsTasks.new(ARGV).run_command!(command)

他的作用是扩展命令别名,如果输入rails s会在aliases查找到对应的命令,相当于执行 rails server

接着加载rails/commands/commands_tasks 并执行Rails::CommandsTasks.new(ARGV).run_command!(command)

1
2
3
4
5
6
7
8
9
10
11
12
def initialize(argv)
@argv = argv
end
def run_command!(command)
command = parse_command(command)
if COMMAND_WHITELIST.include?(command)
send(command)
else
write_error_message(command)
end
end

接着通过send(command)方法会执行server方法,然后运行 Rails::Server.new.start

1
2
3
4
5
6
7
8
9
10
11
12
def server
set_application_directory!
require_command!("server")
Rails::Server.new.tap do |server|
# We need to require application after the server sets environment,
# otherwise the --environment option given to the server won't propagate.
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
end
end

rails/commands/server,Rails::Server 类继承自Rack::Server 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def initialize(*)
super
set_environment
end
def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
log_to_stdout if options[:log_stdout]
super
ensure
# The '-h' option calls exit before @options is set.
# If we call 'options' with it unset, we get double help banners.
puts 'Exiting' unless @options && options[:daemonize]
end

当调用Rails::Server.new方法时,会调用Rack::Server类的initialize方法。config/application.rb 文件加载完成后,会调用 server.start 方法。
这个时候rails通过print_boot_information方法第一次输出信息,并为 INT 信号创建了一个回调,只要在服务器运行时按下 CTRL-C,服务器进程就会退出。用Signal.list可以查看支持的信号类型
并且会创建tmp等文件。super 方法会调用 Rack::Server.start 方法。
lib/rack/server.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def initialize(options = nil)
@ignore_options = []
if options
@use_default_options = false
@options = options
@app = options[:app] if options[:app]
else
argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
@use_default_options = true
@options = parse_options(argv)
end
end
def parse_options(args)
# Don't evaluate CGI ISINDEX parameters.
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?(REQUEST_METHOD)
@options = opt_parser.parse!(args)
@options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
@options
end
def opt_parser
Options.new
end
def start &blk
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
if library = options[:require]
require library
end
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
check_pid! if options[:pid]
# Touch the wrapped app, so that the config.ru is loaded before
# daemonization (i.e. before chdir, etc).
wrapped_app
daemonize_app if options[:daemonize]
write_pid if options[:pid]
trap(:INT) do
if server.respond_to?(:shutdown)
server.shutdown
else
exit
end
end
server.run wrapped_app, options, &blk
end

我们来看下wrapped_app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def wrapped_app
@wrapped_app ||= build_app app
end
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
private
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
self.options.merge! options
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end

options[:config] 的默认值为 config.ru,此文件

1
2
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application

Rack::Builder.parse_file 方法读取 config.ru 文件的内容,并执行

1
2
3
4
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application

config/environment.rb

1
2
3
4
5
# Load the Rails application.
require File.expand_path('../application', __FILE__)
# Initialize the Rails application.
Rails.application.initialize!

config/application.rb

1
2
3
4
5
6
7
8
require File.expand_path('../boot', __FILE__)
ENV['NLS_LANG'] = 'AMERICAN_AMERICA.UTF8'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

加载rails依赖,以及gem
railties/lib/rails/application.rb

1
2
3
4
5
6
def initialize!(group=:default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end

railties/lib/rails/initializable.rb

1
2
3
4
5
6
7
def run_initializers(group=:default, *args)
return if instance_variable_defined?(:@ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
@ran = true
end

Rails 会遍历所有类的祖先,以查找能够响应 initializers 方法的类。对于找到的类,首先按名称排序,然后依次调用 initializers 方法。应用初始化完成后,程序执行流程再次回到 Rack::Server 类。
Rack:lib/rack/server.rb

1
server.run wrapped_app, options, &blk

总结一下,看看rails s启动都加载了哪些类和执行了哪些方法

1. 执行railties/exe/rails 加载cli文件

2. cli文件加载rails/app_rails_loader执行Rails::AppRailsLoader.exec_app_rails方法来遍历查找项目路径下的bin/rails可执行程序,找到后执行

3. bin/rails文件加载config/boot.rb(设置gem的加载路径)文件,并加载rails/commands

4. rails/commands设置了命令的扩展别名,加载rails/commands/commands_tasks类,并执行Rails::CommandsTasks.new(ARGV).run_command!(command)。

5. commands/commands_tasks 判断命令是否为预设的命令,如果不存在,就输入错误信息,如果存在执行对应的方法 server,server加载config/application 并初始化Rails::Server类执行start方法。

6. rails/commands/server start方法,输出项目信息,并为INT信号创建陷阱,还会创建tmp等文件内容,super继承父类,继续执行继承自父类Rack::Server的start方法。

7. lib/rack/server.rb wrapped_app方法会加载config.ru(加载config/envirment(加载config/application(加载rails/all和gems)并初始化Rails.application.initialize!),并执行run Rails.application)返回app,server.run 方法的实现方式取决于我们所使用的服务器,如果使用的是puma实际执行的是Rack::Handler::Puma.run进入服务器端口监听循环。

参考文档: rails中文指南 rails s 启动过程分析